#include "ParserDateTime.h"

const string ParserDateTime::EMPTY_STRING = "";
const string ParserDateTime::DELIMITERS_DATE_TIME = 
	"[,]";
const string ParserDateTime::DATE_TIME_FORMAT =
	"\\s*(.+?)\\s*"
	"(?:" + ParserDateTime::DELIMITERS_DATE_TIME + 
	"\\s*(.+?)\\s*)?";

const string ParserDateTime::DELIMITERS =
	"[:/ .-]";
const string ParserDateTime::DELIMITED_FULL_DATE_FORMAT = 
	"\\s*" 
	"([[:alnum:]]+)" +
	ParserDateTime::DELIMITERS + "+" +
	"([[:alnum:]]+)" +
	ParserDateTime::DELIMITERS + "+" +
	"([[:alnum:]]+)" +
	"\\s*";
const string ParserDateTime::MONTH_FORMATS[NUM_MONTHS] = { 
	"january|jan|01|1", 
	"february|feb|02|2", 
	"march|mar|03|3", 
    "april|apr|04|4", 
	"may|may|05|5", 
	"june|jun|06|6", 
    "july|jul|07|7", 
	"august|aug|08|8", 
	"september|sep|sept|09|9", 
    "october|oct|10", 
	"november|nov|11", 
	"december|dec|12" 
};

const int ParserDateTime::BOOST_DAYS[NUM_DAYS_IN_WEEK] = 
	{Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday};

const string ParserDateTime::DELIMITED_PARTIAL_DATE_FORMAT = 
	"\\s*" 
	"([[:alnum:]]+)" +
	ParserDateTime::DELIMITERS + "+" +
	"([[:alnum:]]+)" +
	"\\s*";

const string ParserDateTime::NUMERIC_TIME_FORMAT = 
	"\\s*"
	"([[:d:]]+)" 
	"\\s*";

const string ParserDateTime::DELIMITERS_TIME = "[:.]";
const string ParserDateTime::DELIMITED_TIME_FORMAT = 
	"\\s*([[:d:]]+)" + DELIMITERS_TIME + "([[:d:]]+)\\s*";
const int ParserDateTime::DELIMITED_TIME_FORMAT_POS_HOURS = 1;
const int ParserDateTime::DELIMITED_TIME_FORMAT_POS_MINUTES = 2;

const int ParserDateTime::DELIMITED_FULL_DATE_POS_YEAR[NUM_DELIMITED_FULL_DATE_FORMATS] = {3, 1};
const int ParserDateTime::DELIMITED_FULL_DATE_POS_MONTH[NUM_DELIMITED_FULL_DATE_FORMATS] = {2, 2};
const int ParserDateTime::DELIMITED_FULL_DATE_POS_DAY[NUM_DELIMITED_FULL_DATE_FORMATS] = {1, 3};

const string ParserDateTime::DAY_FORMATS[NUM_DAYS_IN_WEEK] = { 
	"sun|sunday",
	"mon|monday",
	"tues|tue|tuesday",
	"wed|wednesday",
	"thu|thur|thurs|thursday",
	"fri|friday|",
	"sat|saturday"
};

ptime ParserDateTime::parseDateTime(string input) {
	ptime parsedDate;
	time_duration parsedTime = seconds(0);
	ptime combinedDateTime;

	string stringDate, stringTime;

	// Check if there is a time component available
	bool containsTime;
	containsTime = separateDateFromTime(input, stringDate, stringTime);
	
	// Parse the time first, if it was included
	if (containsTime) {
		parsedTime = parseTime(stringTime);	
		if (parsedTime.is_not_a_date_time()) {
			return not_a_date_time;
		}
	}
	
	// Parse the date portion 
	parsedDate = parseDate(stringDate);

	if (!parsedDate.is_not_a_date_time()) {
		combinedDateTime = parsedDate + parsedTime;
	}else {
		// attempt to parse as time only
		ptime timeOnly = 
			ptime(date(second_clock::local_time().date())) + parseTime(input);
		if (!timeOnly.is_not_a_date_time()) {
			combinedDateTime = timeOnly;
		}else {
			combinedDateTime = not_a_date_time;
		}
	}
	
	return combinedDateTime;
}

bool ParserDateTime::separateDateFromTime(string input, string& date, string& time) {
	regex dateTimeFormat(DATE_TIME_FORMAT);

	date = EMPTY_STRING;
	time = EMPTY_STRING;
	smatch matchingResult;

	if (!regex_match(input, matchingResult, dateTimeFormat)) {
		date = EMPTY_STRING;
		time = EMPTY_STRING;
		return false;
	}

	assert(matchingResult.size() > DATE_TIME_POS_DATE);
	
	date = matchingResult[DATE_TIME_POS_DATE];

	if (matchingResult[DATE_TIME_POS_TIME].matched) {
		time = matchingResult[DATE_TIME_POS_TIME];
		return true;
	}

	return false;
}

time_duration ParserDateTime::parseTime(string input) {
	time_duration timeParsed = not_a_date_time;

	// Try to parse as Numeric time
	timeParsed = parseNumericTime(input);
	if (timeParsed != not_a_date_time) {
		return timeParsed;
	}

	// Try to pasrse as Delimieted Time
	timeParsed = parseDelimitedTime(input);
	if (timeParsed != not_a_date_time) {
		return timeParsed;
	}

	// Developers may add additional formats here.

	return timeParsed;
}

ptime ParserDateTime::parseDate(string input) {
	date returnedDate;
	// Try to parse as delimited full date
	returnedDate = parseDelimitedFullDate(input);
	// Try to parse as partial date
	if (returnedDate.is_not_a_date()) {
		returnedDate = parseDelimitedPartialDate(input);
	}
	// Try to parse as a special date
	if (returnedDate.is_not_a_date()) {
		returnedDate = parseSpecialDate(input);
	}

	// Developers may add additional date formats here

	return ptime(returnedDate);
}

date ParserDateTime::parseDelimitedFullDate(string input) {
	date parsedDate;

	regex fullDateFormat(DELIMITED_FULL_DATE_FORMAT);
	smatch matchResults;

	// Attempt to match this to a recognized date format
	if (!regex_match(input, matchResults, fullDateFormat)) {
		return date(not_a_date_time);
	}

	for (int x = 0; x < NUM_DELIMITED_FULL_DATE_FORMATS; x++) {
		try {
		parsedDate = date(parseYear(matchResults[DELIMITED_FULL_DATE_POS_YEAR[x]]),
							parseMonth(matchResults[DELIMITED_FULL_DATE_POS_MONTH[x]]),
							parseDay(matchResults[DELIMITED_FULL_DATE_POS_DAY[x]]));
		}catch(std::exception) { // exception is thrown when date is not valid
			continue;
		}
		// break out of loop if we found a valid parsing format
		if (!parsedDate.is_not_a_date()) {
			break;
		}
	}
	
	return parsedDate;
}

date ParserDateTime::parseDelimitedPartialDate(string input) {
	date parsedDate;

	regex partialDateFormat(DELIMITED_PARTIAL_DATE_FORMAT);
	smatch matchResults;

	// Attempt to match this to a partial date format
	if (!regex_match(input, matchResults, partialDateFormat)) {
		return date(not_a_date_time);
	}

	try {
		parsedDate = date(second_clock::local_time().date().year(),
			parseMonth(matchResults[DELIMITED_PARTIAL_DATE_POS_MONTH]),
			parseDay(matchResults[DELIMITED_PARTIAL_DATE_POS_DAY]));
	}catch(std::exception) { // exception is thrown when date is not valid
		parsedDate = date(not_a_date_time);
	}
	
	// return closest date/time that has not passed
	if (!parsedDate.is_not_a_date()) {
		if (parsedDate < second_clock::local_time().date()) {
			parsedDate += years(1);
		}
		return parsedDate;
	}else {
		return date(not_a_date_time);
	}
}

date ParserDateTime::parseSpecialDate(string input) {
	for (int dayIndex = 0; dayIndex < NUM_DAYS_IN_WEEK; dayIndex++) {
		regex dayFormat(DAY_FORMATS[dayIndex]);
		smatch matchResults;
		if (regex_match(input, matchResults, dayFormat)) {
			return next_weekday(second_clock::local_time().date(), 
								greg_weekday(BOOST_DAYS[dayIndex]));
		}
	}

	return date(not_a_date_time);
}

time_duration ParserDateTime::parseNumericTime(string input) {

	// Extract relevant parts of the numeric time
	regex timeFormat(NUMERIC_TIME_FORMAT);
	smatch matchResults;
	if (!regex_match(input, matchResults, timeFormat)) {
		return not_a_date_time;
	}

	int time;
	int hours;
	int minutes;

	// Extract numeric time from string
	bool successExtractNumericTime = extractDecimalNumber(input, time);
	assert(successExtractNumericTime);

	// Extract hours and minutes from numbers
	extractHoursMinutes(time, hours, minutes);
	
	return computeTime(hours, minutes);
}

time_duration ParserDateTime::parseDelimitedTime(string input) {
	regex timeFormat(DELIMITED_TIME_FORMAT);
	smatch matchResults;
	if (!regex_match(input, matchResults, timeFormat)) {
		return not_a_date_time;
	}

	int hours;
	int minutes; 

	bool extractSuccess = 
		extractDecimalNumber(matchResults[DELIMITED_TIME_FORMAT_POS_HOURS], hours) && 
		extractDecimalNumber(matchResults[DELIMITED_TIME_FORMAT_POS_MINUTES], minutes);

	if (!extractSuccess) {
		return not_a_date_time;
	}

	return computeTime(hours, minutes);
}

void ParserDateTime::extractHoursMinutes(int numericTime, 
										int& hours, 
										int& minutes) {
	
	if (numericTime < NUMERIC_TIME_MODULO) {
		hours = numericTime;
		minutes = 0;
	}else {
		hours = numericTime / NUMERIC_TIME_MODULO;
		minutes = numericTime % NUMERIC_TIME_MODULO;
	}
}

time_duration ParserDateTime::computeTime(int hours, int minutes) {
	// Ensure that time is a valid time
	if (hours >= NUM_HOURS || minutes >= NUM_MINUTES) {
		return not_a_date_time;
	}else {
		return time_duration(hours, minutes, 0, 0);
	}
}

int ParserDateTime::parseMonth(string input) {
	int month = INVALID_MONTH;
	for (int x = 0; x < NUM_MONTHS; x++) {
		regex rgx(MONTH_FORMATS[x], std::tr1::regex_constants::icase);
		if (regex_match(input, rgx)) {
			month = x + 1;
		}
	}
	return month;
}
int ParserDateTime::parseDay(string input) {
	int day;
		
	if (!extractDecimalNumber(input, day)) {
		day = INVALID_DAY;
	}

	return day;
}
int ParserDateTime::parseYear(string input) {
	int year = INVALID_YEAR;
	
	if (!extractDecimalNumber(input, year)) {
		year = INVALID_YEAR;
	}else {
		// handles cases where the user uses a shortened form for the year
		if (year >= MIN_YEAR && year < MAX_SHORTENED_YEAR) {
			year = year + CUTOFF_REF_YEAR;
		}
	}
	
	return year;
}

bool ParserDateTime::extractDecimalNumber(string input, int& output) {
	stringstream strStream(input);

	strStream >> output;
	strStream >> ws;

	bool validInteger = !(strStream.fail() || !strStream.eof());

	return validInteger;
}